package il.technion.cs236369.proxy;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Properties;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;

import org.apache.http.HttpException;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.RequestContent;
import org.apache.http.protocol.RequestExpectContinue;
import org.apache.http.protocol.RequestTargetHost;
import org.apache.http.protocol.RequestUserAgent;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

/**
 * Supplied HttpProxy Class.
 * @author Gil Fruchter 037023231 sgilf@t2
 *
 */
public class HttpProxy {
	static Properties props;
	SocketFactory sockFactory;
	ServerSocketFactory srvSockFactory;
	int port;
	HttpService httpService;
	ServerSocket serverSocket;
	HttpParams params;
	HttpProxyHandler handler;
	/**
	 * Constructs the proxy
	 * 
	 * @param sockFactory The SocketFactory to be used for creating new sockets for connecting
	 * to servers
	 * 
	 * @param srvSockFactory The ServerSocketFactory to be used for creating a single ServerSocket for
	 * listening for clients requests
	 * 
	 * @param port The port number to bounded by the ServerSocket
	 * 
	 * @param dbURL url of the database (e.g. jdbc:mysql://127.0.0.1:3306/)
	 * @param dbName The name of the database (e.g. proxy)
	 * @param tblName The name of the table in the database (e.g. cache)
	 * @param dbUsername Database's username (e.g. root)
	 * @param dbPassword Database's password
	 * @param dbDriver Database's driver class name (com.mysql.jdbc.Driver)
	 */
	
	@Inject
	HttpProxy(
			SocketFactory sockFactory,
			ServerSocketFactory srvSockFactory,
			@Named("httproxy.net.port") int port,
			@Named("httproxy.db.url") String dbURL,
			@Named("httproxy.db.name") String dbName,
			@Named("httproxy.db.table") String tblName,
			@Named("httproxy.db.username") String dbUsername,
			@Named("httproxy.db.password") String dbPassword,
			@Named("httproxy.db.driver") String dbDriver) {
		//save required params
		this.sockFactory = sockFactory;
		this.srvSockFactory = srvSockFactory;	
		this.port = port;
		this.httpService = null;
		this.serverSocket = null;
		
		props = new Properties();
		props.setProperty("httproxy.net.port", String.valueOf(port));
		props.setProperty("httproxy.db.url", dbURL);
		props.setProperty("httproxy.db.name", dbName);
		props.setProperty("httproxy.db.table", tblName);
		props.setProperty("httproxy.db.username", dbUsername);
		props.setProperty("httproxy.db.password", dbPassword);
		props.setProperty("httproxy.db.driver", dbDriver);
		
		
		this.params = new BasicHttpParams();
        this.params
            .setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
            .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 10 * 1024)
            .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
            .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
            .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "HttpComponents/1.1");
        
	}
	
	
	/**
	 * Create a new bounded server socket using ServerSocketFactory with the given port
	 * @throws IOException unable to bind the server socket
	 */
	public void bind() throws IOException {
		// Create server socket listening on given port
		this.serverSocket = srvSockFactory.createServerSocket(port);
        
        // Set up HTTP protocol processor for incoming connections
        BasicHttpProcessor inhttpproc = new BasicHttpProcessor();
        inhttpproc.addInterceptor(new ResponseDate());
        inhttpproc.addInterceptor(new ResponseServer());
        inhttpproc.addInterceptor(new ResponseContent());
        inhttpproc.addInterceptor(new ResponseConnControl());
        
        // Set up HTTP protocol processor for outgoing connections
        BasicHttpProcessor outhttpproc = new BasicHttpProcessor();
        outhttpproc.addInterceptor(new RequestContent());
        outhttpproc.addInterceptor(new RequestTargetHost());
        outhttpproc.addInterceptor(new RequestConnControl());
        outhttpproc.addInterceptor(new RequestUserAgent());
        outhttpproc.addInterceptor(new RequestExpectContinue());
        
        // Set up outgoing request executor 
        HttpRequestExecutor httpexecutor = new HttpRequestExecutor();
        
        // Set up incoming request handler
        HttpRequestHandlerRegistry reqistry = new HttpRequestHandlerRegistry();
        handler = new HttpProxyHandler(outhttpproc, httpexecutor, this.sockFactory, props);
        reqistry.register("*", handler);
        
        // Set up the HTTP service
        this.httpService = new HttpService(inhttpproc, 
        		new DefaultConnectionReuseStrategy(), 
        		new DefaultHttpResponseFactory(), 
        		reqistry, 
        		params);               
       
	}
	
	
	
	/**
	 * Starts the server loop:
	 * listens to client requests and executes them.
	 * To create new sockets for connecting to servers use ONLY SocketFactory.createSocket(String host, int port)
	 * where SocketFactory is the one passed to the constructor.
	 */
	public void start() {
		System.out.println("Proxy is running");
		while(true) {
			Socket insocket = null;
			DefaultHttpServerConnection inconn = null;
			try {
				try {
					System.out.println("Accepting connection...");
					insocket = this.serverSocket.accept();
				} catch (IOException e1) {
					System.err.println("Unable to accept incoming socket");
					continue;
				}
				inconn = new DefaultHttpServerConnection();
				System.out.println("Incoming connection from " + insocket.getInetAddress());
				try {
					inconn.bind(insocket, this.params);
				} catch (IOException e1) {
					System.err.println("Unable to bind incoming socket, closing the socket...");
					if (insocket != null) {
						try {
							insocket.close();
						} catch (IOException e) {
							System.err.println("Unable to close incoming socket");
						}
					}
					continue;
				}
				HttpContext context = new BasicHttpContext(null);
				try {
					this.httpService.handleRequest(inconn, context);
				} catch (IOException | HttpException e) {
					e.printStackTrace();
					System.err.println("Unable to handle request");
				}
			} finally {
//				try {
//					if (insocket != null) {
//						insocket.close();
//					}
//				} catch (IOException e) {
//					System.err.println("Unable to close incoming socket");
//				}
				try {
					if (inconn != null) {
						inconn.close();
					}
				} catch (IOException e) {
					System.err.println("Unable to close incoming connection");
				}
				handler.closeConnections();
			}
		}
	}
	
	
	public static void main(String[] args) throws Exception {
		// first arg is the path to the config file
		props = new Properties();
		props.load(new FileInputStream(args[0]));
		Injector injector = Guice.createInjector(new HttpProxyModule(props));
		final HttpProxy proxy = injector.getInstance(HttpProxy.class);
		proxy.bind();
		proxy.start();
		
	}
}

